package com.davidlife.javaee.redis.cases;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.ZParams;

import java.util.*;

/*
### 案例1
> 构建一个简单的文章投票功能的网站的后端。（参考《Redis实战》P15）

* 一篇文章获得大于200张支持票(up vote)则认为这是有趣的文章
* 把有趣的文章显示到文章列表的前面
* **如何计算文章的评分**：根据文章发布的时间和得票数计算出文章的得分
    * 用文章的发布时间 + （文章的支持票数 * 一个常数），得到的就是文章的评分。（其中这个常数用一天的秒数86400除以200得到(432)，文章每获得一张支持票，程序就将文章的评分增加432分）
* **存储的数据结构设计**：使用Redis存储各种信息：
    1. 对于每篇文章，使用一个散列(Hash)，以article:92617（92617为文章ID）为key，来存储文章名称、指向文章的网址、发布的用户、发布时间、得到的投票数等信息（使用 : 作为分隔符在Redis中很常用，后面会介绍使用冒号创建嵌套命名空间的方法）
    2. 使用两个有序集合（sortedset）来有序的存储文章：
        1. 第一个有序集合的成员为文章ID，分值为文章的发布时间
        2. 第二个有序集合的成员同样为文章ID，分值为文章的评分
        * 通过这两个有序集合，网站既可以根据文章发布的先后顺序来展示文章，又可以根据网站的评分高低来展示文章
    3. 为了防止用户对同一篇文章进行多次投票，网站需要为每篇文章记录一个已投票用户名单。为此，程序为每篇文章创建一个集合(set)，并使用这个集合来存储所有已投票用户的ID。(如，user:234487 user:253378 等)
        * 另外，为了尽量节约内存，我们规定当一篇文章发布期满一周之后，用户将不能再对它进行投票，文章的评分将被固定下来，而记录文章已投票用户名单的集合也会被删除
* 代码实现
    1. 投票功能
    2. 发布并获取文章
    3. 群组功能：对文章进行分组，可以让用户只看见与特定话题有关的文章
        * 需要为每个群组创建一个集合，并将所有同属于一个群组的文章ID都记录到那个集合里面
* [参考代码-文章投票功能的实现](https://gitee.com/davidlife/knowledge-javaEE-demo/blob/master/src/com/davidlife/javaee/redis/cases/ArticleVoting.java)
* 几个注意点：
    1. `zinterstore`命令可以接受多个集合和多个有序集合作为输入，找出所有同时存在于集合和有序集合的成员，并以几种不同的方式来合并(combine)这些成员的分值（比如使用两个集合中相同成员中的最大的那个分值来作为交集成员的分值）。（对于集合成员，它没有分值，所以就把他们的分值都视为是1，而对于有序集合当然就直接使用它的成员的分值）
        * 如果集合中包含的内容非常多，那么执行`zinterstore`命令就会比较花时间，为了尽量减少Redis的工作量，可以将这个命令的计算结果缓存60秒
    2. Redis Zinterstore 命令计算给定的一个或多个有序集的交集，其中给定 key 的数量必须以 numkeys 参数指定(即有几个集合要合并)，并将该交集(结果集)储存到 destination 。 默认情况下，结果集中某个成员的分数值是所有给定集下该成员分数值之和。
        ```
        ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
        例：ZINTERSTORE sum_point 2 mid_test fin_test （mid_test和fin_test是两个zset）
        ```
* 到这里应该注意到，Redis使我们思考数据问题的方式发生了变化，从原来的“怎样将我的想法塞进数据库的表和行里面”，变成了“使用哪种Redis数据结构来解决这个问题比较好呢？”
 */
public class ArticleVoting {

    public static final int ONE_WEEK_IN_SECONDS = 7 * 86400;
    private static final int VOTE_SCORE = 432;
    private static final int ARTICLES_PER_PAGE = 25;

    public static void main(String[] args) {
        new ArticleVoting().run();
    }

    public void run() {
        Jedis conn = new Jedis("localhost", 6379);

        String articleId = postArticle(conn, "username", "A title", "http://www.baidu.com");
        String article = "article:" + articleId;
        System.out.println("We posted a new article with id: " + articleId + ". It's key is " + article);
        System.out.println("Its HASH looks like:");
        Map<String, String> articleData = conn.hgetAll(article);
        Set<Map.Entry<String, String>> entries = articleData.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            System.out.println("  " + entry.getKey() + ": " + entry.getValue());
        }

        System.out.println();

        articleVote(conn, "other_user", article);
        String votes = conn.hget("article:" + articleId, "votes");
        System.out.println("We voted for the article, it now has votes: " + votes);
        assert Integer.parseInt(votes) > 1;

        System.out.println();

        System.out.println("The currently highest-scoring articles are:");
        List<Map<String,String>> articles = getArticles(conn, 1);
        printArticles(articles);
        assert articles.size() >= 1;

        System.out.println();

        addGroups(conn, articleId, new String[]{"new-group"});
        System.out.println("We added the article to a new group, other articles include:");
        articles = getGroupArticles(conn, "new-group", 1);
        printArticles(articles);
        assert articles.size() >= 1;

        conn.close();
    }

    //发布文章
    public String postArticle(Jedis conn, String user, String title, String link) {
        String articleId = String.valueOf(conn.incr("article:"));  //key名称为"article:"的string增1操作

        //规定一周后用户不能评论，所以，文章的投票用户名单应该失效，为了尽可能节约内存
        //让Redis可以在文章发布期满一周之后自动删除这个集合
        String voted = "voted:" + articleId;
        conn.sadd(voted, user);
        conn.expire(voted, ONE_WEEK_IN_SECONDS);

        long now = System.currentTimeMillis() / 1000;
        String article = "article:" + articleId;
        HashMap<String, String> articleData = new HashMap<>();
        articleData.put("title", title);
        articleData.put("link", link);
        articleData.put("user", user);
        articleData.put("now", String.valueOf(now));
        articleData.put("votes", "1");
        conn.hmset(article, articleData);
        conn.zadd("score:", now + VOTE_SCORE, article);
        conn.zadd("time:", now, article);

        return articleId;
    }

    //投票功能的实现
    /*
     注意：从技术上讲，要正确的实现投票功能，需要将下面的sadd、zincrby、hincrBy 这3个命令放到一个事务里面执行，这里为了简单，暂时忽略这个问题。后面讲Redis事务时再详述。
     */
    public void articleVote(Jedis conn, String user, String article) {
        long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
        if (conn.zscore("time:", article) > cutoff) {
            return;
        }

        String articleId = article.substring(article.indexOf(":") + 1);
        if (conn.sadd("voted:" + articleId) == 1) {
            conn.zincrby("score:", VOTE_SCORE, article);
            conn.hincrBy(article, "votes", 1);
        }
    }

    //展示最新发布的文章或评分最高的文章
    public List<Map<String, String>> getArticles(Jedis conn, int page) {
        return getArticles(conn, page, "score:");
    }

    public List<Map<String, String>> getArticles(Jedis conn, int page, String order) {
        int start = (page - 1) * ARTICLES_PER_PAGE;
        int end = start + ARTICLES_PER_PAGE - 1;

        Set<String> ids = conn.zrevrange(order, start, end);
        List<Map<String, String>> articles = new ArrayList<>();
        for (String id : ids) {
            Map<String, String> articleData = conn.hgetAll(id);
            articleData.put("id", id);
            articles.add(articleData);
        }
        return articles;
    }

    //把文章加到几个不同的group(群组)中
    public void addGroups(Jedis conn, String articleId, String[] groupsToAdd) {
        String article = "article:" + articleId;
        for (String group : groupsToAdd) {
            conn.sadd("group:" + group, article);
        }
    }

    //群组功能：对文章进行分组，即可以让用户只看见与特定话题有关的文章
    public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page) {
        return getGroupArticles(conn, group, page, "score:");
    }

    public List<Map<String, String>> getGroupArticles(Jedis conn, String group, int page, String order) {
        String key = order + group;
        if (!conn.exists(key)) {
            ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
            conn.zinterstore(key, params, "group:" + group, order);
            conn.expire(key, 60);
        }
        return getArticles(conn, page, key);
    }

    private void printArticles(List<Map<String,String>> articles){
        for (Map<String,String> article : articles){
            System.out.println("  id: " + article.get("id"));
            for (Map.Entry<String,String> entry : article.entrySet()){
                if (entry.getKey().equals("id")){
                    continue;
                }
                System.out.println("    " + entry.getKey() + ": " + entry.getValue());
            }
        }
    }
}
